package se.webbzon.boltzmann.battle.round;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import se.webbzon.boltzmann.battle.battleaction.BattleAction;
import se.webbzon.boltzmann.battle.group.BattleGroup;
import se.webbzon.boltzmann.battle.group.BattleGroupListener;
import se.webbzon.boltzmann.battle.object.npc.BattleNPC;
import se.webbzon.boltzmann.battle.property.BattleBuff;
import se.webbzon.boltzmann.battle.property.BattleProperty;
import se.webbzon.boltzmann.battle.strategy.AbstractStrategy;
import se.webbzon.boltzmann.battle.strategy.StrategyListener;
import se.webbzon.oschi01.friendly.Friendly;
import se.webbzon.oschi01.worldarea.WorldArea;

public class BattleRound {
	
	// The world area associated with this battle round
	private final WorldArea world;
	
	// The participating groups in this battle round
	private final List<BattleGroup> groups;
	
	// The listeners to this battle round
	private final Collection<BattleRoundListener> listeners;
	
	// The strategy listener used by the battle round
	private final StrategyListener strategyListener;
	
	// The current group and participant in the group
	private int groupIndex;
	private int participantIndex;
	
	// The current non-player battle character
	private BattleGroup group;
	private BattleNPC participant;
	
	// The number of rounds and turns since the battle began
	private int round, turn;
	
	// Whatever the battle round is paused or not
	private boolean paused;
	
	// Whatever the battle round is currently within a turn
	private boolean withinTurn;
	
	/*============================================================
	Constructors
	============================================================*/
	
	/** Creates a new battle round with a given list of battle groups. **/
	public BattleRound(WorldArea world, List<BattleGroup> groups) {
		this.world = world;
		this.groups = groups;
		listeners = new ArrayList<BattleRoundListener>();
		strategyListener = new BattleRoundStrategyListener(this);
		paused = false;
		withinTurn = false;
		groupIndex = 0;
		participantIndex = 0;
		round = 0;
		turn = 0;
		
		final BattleGroupListener battleGroupListener = 
				new BattleRoundBattleGroupListener(this);
		
		for (int i = 0; i < groups.size(); i++) {
			BattleGroup group = groups.get(i);
			if (group.participants() == 0)
				groups.remove(i);
			else
				group.addListener(battleGroupListener);
		}
	}
	
	/*============================================================
	Public Methods
	============================================================*/
	
	/** Adds a listener to this battle round. **/
	public boolean addListener(BattleRoundListener listener) {
		return listeners.add(listener);
	}
	
	/** Removes a listener from this battle round. **/
	public boolean removeListener(BattleRoundListener listener) {
		return listeners.remove(listener);
	}
	
	/** Clears all listeners from this battle round. **/
	public void clearListeners() {
		listeners.clear();
	}
	
	/** Returns true if the battle round is paused. **/
	public boolean isPaused() {
		return paused;
	}
	
	/** Pauses the battle round. **/
	public void pause() {
		paused = true;
	}
	
	/** Starts or resumes the battle round. **/
	public void start() {
		paused = false;
		if (!withinTurn)
			startTurn();
	}
	
	/** Adds a battle group to this battle round. **/
	public void addGroup(BattleGroup group) {
		groups.add(group);
	}
	
	/** Removes a battle group from this battle round. **/
	public void removeGroup(BattleGroup group) {
		groups.remove(group);
	}
	
	/** Returns a list of all groups in the battle round **/
	public List<BattleGroup> getGroups() {
		return Collections.unmodifiableList(groups);
	}
	
	/** Returns the current active battle group. **/
	public BattleGroup getCurrentGroup() {
		return group;
	}
	
	/** Returns the current active non-player battle character. **/
	public BattleNPC getCurrentParticipant() {
		return participant;
	}
	
	/** Returns the current turn number in the battle round. **/
	public int getTurn() {
		return turn;
	}
	
	/** Returns the current round number in the battle round. **/
	public int getRound() {
		return round;
	}
	
	/*============================================================
	Friendly Methods
	============================================================*/
	
	/** Selects the next non-player battle character in the battle
	 * round and starts the turn for the non-player battle character. **/
	@Friendly void startTurn() {
		withinTurn = true;
		group = groups.get(groupIndex);
		participant = group.getParticipant(participantIndex);
		if (turn == 0) {
			if (round == 0)
				notifyBattleStart(world);
			
			notifyRoundStart();
		}
		
		if (participantIndex == 0)
			notifyGroupStart(group);
		
		notifyTurnStart(participant);
		
		continueTurn(participant,0,false);
	}
	
	/** Continues the turn for the given non-player battle character. The index
	 * of the next buff to cast for the non-player battle character is given by
	 * the input nextBuff. **/
	@Friendly void continueTurn(BattleNPC npc, int nextBuff, boolean block) {
		List<BattleBuff> buffs = npc.getProperty().getBuffs();
		if (nextBuff >= buffs.size()) {
			countdownBuffs(npc);
			if (block)
				endTurn();
			else
				startStrategy(npc);
			
		} else {
			final BattleBuff buff = buffs.get(nextBuff);
			if (buff.isActionBuff()) {
				// Triggers the action for the buff
				ArrayList<BattleNPC> targets = new ArrayList<BattleNPC>();
				targets.add(npc);
				BattleAction buffAction = 
					buff.getActionFactory().makeBattleAction(world,npc,targets);
				buffAction.addListener(new BattleRoundBuffActionListener(this,nextBuff, block | buff.isBlockingBuff()));
				buffAction.start();
				
			} else
				continueTurn(npc, nextBuff+1, block | buff.isBlockingBuff());
		}
	}
	
	/** Ends the turn for current non-player battle character. **/
	@Friendly void endTurn() {
		if (participantIndex == group.participants()-1) {
			participantIndex = 0;
			if (groupIndex == groups.size()-1) {
				groupIndex = 0;
				notifyRoundComplete();
				round++;
				turn = 0;
			} else {
				groupIndex++;
				turn++;
			}
			
			notifyGroupComplete(group);
		} else {
			participantIndex++;
			turn++;
		}
		
		notifyTurnComplete(participant);
		turn++;
		
		// Continue 
		if (!paused)
			startTurn();
		else
			withinTurn = false;
	}
	
	/** Returns true if any faction has the victory of the battle. 
	 * If notifyListeners is set then all listeners to this battle round
	 * will be notified if a faction has the victory of the battle. **/
	@Friendly boolean checkAndNotifyVictory(boolean notifyListeners) {
		final int none = -1;
		int winningFactionId = none;
		
		// Scan all groups for victory
		for (BattleGroup group : groups) {
			if (group.participants() > 0) {
				final int factionId = group.getFactionId();
				if (winningFactionId == none)
					winningFactionId = factionId;
				else if (winningFactionId != factionId )
					return false;
			}
		}
		
		if (winningFactionId == none) 
			return false;
		else if (notifyListeners) 
			notifyBattleComplete(winningFactionId);
		return true;
	}
	
	/*============================================================
	Private Methods
	============================================================*/
	
	/** Notifies to all the listeners that a new turn has started. **/
	private void notifyTurnStart(BattleNPC participant) {
		for (BattleRoundListener listener : listeners)
			listener.turnStart(this,participant);
	}
	
	/** Notifies to all the listeners that the current turn has been completed. **/
	private void notifyTurnComplete(BattleNPC participant) {
		for (BattleRoundListener listener : listeners)
			listener.turnComplete(this,participant);
	}
	
	/** Notifies to all the listeners that a new round has started. **/
	private void notifyRoundStart() {
		for (BattleRoundListener listener : listeners)
			listener.roundStart(this);
	}
	
	/** Notifies to all the listeners that the current round has been completed. **/
	private void notifyRoundComplete() {
		for (BattleRoundListener listener : listeners)
			listener.roundComplete(this);
	}
	
	/** Notifies to all the listeners that a new group has started their turns. **/
	private void notifyGroupStart(BattleGroup group) {
		for (BattleRoundListener listener : listeners)
			listener.groupStart(this, group);
	}
	
	/** Notifies to all the listeners that the current group has 
	 * completed their turns. **/
	private void notifyGroupComplete(BattleGroup group) {
		for (BattleRoundListener listener : listeners)
			listener.groupComplete(this, group);
	}
	
	/** Notifies to all the listeners that the battle has started. **/
	private void notifyBattleStart(WorldArea world) {
		for (BattleRoundListener listener : listeners)
			listener.battleStart(this, world);
	}
	
	/** Notifies to all the listeners that the current battle has been completed. **/
	private void notifyBattleComplete(int winningFactionId) {
		for (BattleRoundListener listener : listeners)
			listener.battleComplete(this, winningFactionId);
	}
	
	/** Starts the strategy for the given non-player battle character. **/
	private void startStrategy(BattleNPC npc) {
		AbstractStrategy strategy = npc.getProperty().getStrategy();
		strategy.removeListener(strategyListener);
		strategy.addListener(strategyListener);
		strategy.onBegin(this, world, groups, participant);
	}
	
	/** Counts down the duration of all battle buffs of the given non-player
	 * battle character. **/
	private void countdownBuffs(BattleNPC npc) {
		final BattleProperty property = npc.getProperty();
		final ArrayDeque<BattleBuff> removeBuffs = new ArrayDeque<BattleBuff>();
		final List<BattleBuff> buffs = property.getBuffs();
		
		// Sweep through all buffs
		for (BattleBuff buff : buffs) {
			final int duration = buff.getDuration();
			if (duration == 1) {
				removeBuffs.add(buff);
			} else if (duration > 1)
				buff.setDuration(duration-1);
		}
		
		// Remove expired buffs
		for (BattleBuff buff : removeBuffs)
			property.removeBuff(buff);
		
	}
	
}
